1 module hip.game2d.text; 2 3 import hip.api.data.font; 4 import hip.api.graphics.text; 5 import hip.util.data_structures; 6 7 8 /** 9 * Formatting the text: 10 * Text should be formatted using the $() syntax. 11 * Currently, no formatting is support, but that syntax is reserved and in the future, it 12 * will be used as for example: $(RGB, 1.0, 1.0, 1.0) or even $(WHITE), so, basic parsing 13 * is being done for accounting how many text does really need to be rendered. 14 */ 15 class HipText 16 { 17 HipTextAlign alignh = HipTextAlign.LEFT; 18 HipTextAlign alignv = HipTextAlign.TOP; 19 20 HipFont font; 21 int x, y; 22 bool wordWrap; 23 24 DirtyFlagsCmp!( 25 shouldUpdateText, x, y, 26 wordWrap, font, 27 alignh, alignv 28 ) checkDirty; 29 30 31 float depth = 0; 32 ///Update dynamically based on the font, the text scale and the text content 33 int width, height; 34 35 int boundsWidth = -1, boundsHeight = -1; 36 37 //Line widths, containing width for each line for correctly aplying text align 38 uint[] linesWidths; 39 40 protected string _text; 41 protected dstring _dtext; 42 protected dstring processedText; 43 protected HipColor _color = HipColor.black; 44 45 //Debugging? 46 47 protected bool shouldRenderSpace = false; 48 protected bool shouldRenderLineBreak = false; 49 50 protected HipTextStopConfig[] textConfig; 51 protected HipTextRendererVertexAPI[] vertices; 52 53 //Caching 54 protected size_t _drawableTextCount = 0; 55 protected size_t maxDrawableTextCount = 0; 56 public bool shouldUpdateText = true; 57 58 this(int boundsWidth = -1, int boundsHeight = -1, bool bWordWrap = false) 59 { 60 import hip.api; 61 checkDirty.start(this); 62 this.font = cast()HipDefaultAssets.getDefaultFont(); 63 linesWidths.length = 1; 64 wordWrap = bWordWrap; 65 this.boundsWidth = boundsWidth; 66 this.boundsHeight = boundsHeight; 67 } 68 this(string text, int x, int y, HipFont fnt = null, int boundsWidth = -1, int boundsHeight = -1, bool bWordWrap = false) 69 { 70 this(boundsWidth, boundsHeight, bWordWrap); 71 this.setPosition(x,y); 72 this.text = text; 73 if(fnt) font = fnt; 74 } 75 string text() const {return _text;} 76 size_t drawableTextCount() const {return _drawableTextCount;} 77 78 79 string text(string newText) 80 { 81 if(newText != _text) 82 { 83 import hip.util.string; 84 dstring dtext = newText.toUTF32; 85 _drawableTextCount = countVertices(dtext); 86 shouldUpdateText = true; 87 if(_drawableTextCount > maxDrawableTextCount) 88 { 89 //As it is a quad, it needs to have vertices * 4 90 vertices.length = _drawableTextCount * 4; 91 maxDrawableTextCount = _drawableTextCount; 92 } 93 _text = newText; 94 _dtext = dtext; 95 } 96 return _text; 97 } 98 99 void setPosition(int x, int y) 100 { 101 this.x = x; 102 this.y = y; 103 } 104 105 HipColor color() => _color; 106 HipColor color(HipColor c) => _color = c; 107 108 void[] getVertices() 109 { 110 checkDirty(); 111 if(shouldUpdateText) 112 { 113 updateText(font); 114 checkDirty.start(this); 115 } 116 117 return cast(void[])vertices[0..drawableTextCount * 4]; 118 } 119 120 protected void updateAlign(int lineNumber, out int displayX, out int displayY, int boundsWidth, int boundsHeight) 121 { 122 import hip.api.graphics.text; 123 getPositionFromAlignment(x, y, linesWidths[lineNumber], height, alignh, alignv, displayX, displayY, boundsWidth, boundsHeight); 124 } 125 126 127 public void getSize(out int width, out int height) 128 { 129 if(processedText == null) 130 HipTextStopConfig.parseText(_dtext, processedText, textConfig); 131 font.calculateTextBounds(processedText, linesWidths, width, height, boundsWidth); 132 this.width = width; 133 this.height = height; 134 } 135 public void setAlign(HipTextAlign alignh, HipTextAlign alignv) 136 { 137 this.alignh = alignh; 138 this.alignv = alignv; 139 } 140 141 package void updateText(IHipFont font) 142 { 143 HipTextStopConfig.parseText(_dtext, processedText, textConfig); 144 int vI = 0; //vertex buffer index 145 146 bool isFirstLine = true; 147 int yoffset = 0; 148 foreach(HipLineInfo lineInfo; font.wordWrapRange(processedText, wordWrap ? boundsWidth : -1)) 149 { 150 if(!isFirstLine) 151 { 152 yoffset+= font.lineBreakHeight; 153 } 154 isFirstLine = false; 155 int xoffset = 0; 156 int displayX = void, displayY = void; 157 getPositionFromAlignment(x, y, lineInfo.width, height, alignh, alignv, displayX, displayY, boundsWidth, boundsHeight); 158 for(int i = 0; i < lineInfo.line.length; i++) 159 { 160 int kerning = lineInfo.kerningCache[i]; 161 const(HipFontChar)* ch = lineInfo.fontCharCache[i]; 162 163 switch(lineInfo.line[i]) 164 { 165 case ' ': 166 if(!shouldRenderSpace) 167 { 168 xoffset+= font.spaceWidth; 169 break; 170 } 171 goto default; 172 default: 173 if(ch is null) continue; 174 ch.putCharacterQuad( 175 cast(float)(xoffset+displayX+ch.xoffset+kerning), 176 cast(float)(yoffset+displayY+ch.yoffset), depth, 177 vertices[vI..vI+4] 178 ); 179 vI+= 4; 180 xoffset+= ch.xadvance; 181 } 182 } 183 } 184 shouldUpdateText = false; 185 } 186 187 void draw() 188 { 189 import hip.api.graphics.g2d.g2d_binding; 190 setTextColor(color); 191 drawTextVertices(getVertices, font); 192 } 193 } 194 195 196 /** 197 * The text stop config defines how this text will behave a 198 */ 199 package struct HipTextStopConfig 200 { 201 import hip.api.graphics.color; 202 int startIndex; 203 HipColorf color; 204 205 //This is just a plan, not supported right now 206 public static enum Tokens 207 { 208 alignh = "alignh", 209 alignv = "alignv", 210 rgb = "rgb", 211 color = "color", 212 bold = "bold", 213 italic = "italic", 214 red = "red", 215 green = "green", 216 blue = "blue", 217 } 218 219 private static HipTextStopConfig parseToken(in dstring text, size_t indexToParse, out size_t continueIndex) 220 { 221 import hip.util.conv; 222 import hip.util.string; 223 import hip.util.algorithm; 224 int endIndex = text[indexToParse..$].indexOf(")"); //Won't support parenthesis between them. 225 assert(endIndex != -1, "Missing ending parenthesis on string at HipTextStopConfig formatting "); 226 continueIndex = endIndex+indexToParse; 227 228 229 auto range = splitRange(text[indexToParse..endIndex], ","); 230 dstring token = range.front; 231 range.popFront(); 232 233 switch(token) 234 { 235 case "rgb": 236 { 237 HipColorf c = HipColorf(0, 0, 0, 1.0); 238 range.map!((x) => x.trim.to!float).put(&c.r, &c.g, &c.b); 239 return HipTextStopConfig(cast(int)indexToParse, c); 240 } 241 default: break; 242 } 243 return HipTextStopConfig(cast(int)indexToParse, cast()HipColorf.white); 244 } 245 246 247 static void parseText(in dstring text, out dstring parsedText, ref HipTextStopConfig[] config) 248 { 249 parsedText = text; 250 // size_t indexConfig = 0; 251 // size_t lastParseIndex = 0; 252 // dstring parsingText; 253 // for(size_t i = 0; i < text.length; i++) 254 // { 255 // if(i+1 < text.length && text[i] == '$' && text[i+1] == '(') //Found something to parse 256 // { 257 // parsingText~= text[lastParseIndex..i-1]; 258 // HipTextStopConfig cfg = parseToken(text, i+1, i); //Update i 259 // lastParseIndex = i; 260 // if(indexConfig >= config.length) 261 // config.length++; 262 // config[indexConfig++] = cfg; 263 // } 264 // } 265 // //!FIXME: This allocated on each frame. It should both be used a @nogc operation (String) or it should 266 // //!find a way to create a range to be used instead of a string. 267 // if(lastParseIndex == 0) 268 // { 269 // parsedText = text; 270 // return; 271 // } 272 // parsedText = parsingText ~ text[lastParseIndex..$]; 273 } 274 275 } 276 277 278 279 private size_t countVertices(dstring str) 280 { 281 size_t i = 0; 282 foreach(ch; str) 283 { 284 if(ch != ' ' && ch != '\n') 285 i++; 286 } 287 return i; 288 }